#pragma once
#include"task.hpp"
#include<string>
#include<vector>
#include<cstdlib>
#include<ctime>
#include<cassert>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
//进程池：由于创建进程是有代价的，所以我们可以一次性创建一批进程，然后有任务就交给创建好的子进程做即可


const int processnum=10;//进程数

std::vector<task_t>tasks;

//一切离不开：先描述，再组织

//先描述：想要创建一批子进程并且管理起来，就要有对应的数据结构
class channel//要有一个结构去记录每一个进程的属性，然后把这些结构组织起来，就可以管理一批进程了，也就是进程池
{
public:
    channel(int cmdfd,int slaverid,const std::string &processname)
    :cmdfd_(cmdfd)
    ,slaverid_(slaverid)
    ,processname_(processname)
    {}
    int cmdfd_;//给子进程发送任务的写端的文件描述符(就是对应进程管道的写端)
    int slaverid_;//子进程的pid
    std::string processname_;//子进程的名字

};

//子进程执行的代码
void slaver()
{
    while(true)
    {
        int cmdcode=0;
        //这些管道都有协同的功能，也满足管道的使用的4种情况，所以在父进程没有对子进程管道进行写入的时候，也就是没有派发任务的时候
        //在读写端都正常的时候，子进程的read会阻塞等待父进程写入，也就是进程在等待任务
        ssize_t n=read(0,&cmdcode,sizeof(int));//此时0号已经被重定向成在自己对应的管道中读取数据了，读取到cmdcode里，读取一个整形的大小
        //读取返回的n是成读取的字符个数
        if(n==sizeof(int))//成功读取到数据，开始执行任务
        {
            cout<<"slaver say@get a command"<<getpid()<<":"<<cmdcode<<endl;
            if(cmdcode>=0&&cmdcode<tasks.size())//[]的访问不能超过size的大小
            {
                tasks[cmdcode]();//调用对应的函数执行对应的任务
            }
        }
        if(n==0)//当父进程写端关闭时，子进程的read就不会被阻塞，会一直读到0，此时就会触发这个条件，然后退出执行任务
        {
            break;
        }

    }
}

void InitProcessPool(std::vector<channel>&channels)
{
    //由于创建子进程的时候，子进程会拷贝一份父进程的文件描述符表，所以从第二个子进程开始，都会连接到前面创建的所有子进程的写端
    //但是管道最好是只有一个读一个写的连接，所以我们要手动关闭;

    std::vector<int>oldfds;

    //初始化进程池就是为了创建好一批进程，并且把这些进程的属性形成结构放到channels容器中管理起来
    for(int i=0;i<processnum;i++)
    {
        int pipefd[2];
        int n=pipe(pipefd);//创建管道，给进程添加两个对管道读，写的文件描述符
        assert(!n);//成功返回0
        (void)n;//提示编译器该变量我们故意不使用

        //建立好管道之后就可以fork，这样子进程就继承了管道的描述符
        pid_t id=fork();
        if(id==0)
        {
            //子进程先关闭历史的指向之前创建的子进程的写端
            cout<<"child"<<getpid()<<"close history fd: ";
            for(auto fd:oldfds)
            {
                cout<<fd<<" ";
                close(fd);
            }
            cout<<endl;
            //再关闭自己的这个管道的写端
            close(pipefd[1]);
            //再使用dup2函数(重定向)可以把读端pipefd[0]描述符对应的文件指针覆盖到0号文件描述符的位置，也就是统一让子进程的0号文件描述符位置作为读取管道的位置
            dup2(pipefd[0],0);//统一所有子进程的管道读端的文件描述符为0
            //再关闭旧的读端
            close(pipefd[0]);

            //管道建立完成后子进程开始执行自己的任务
            slaver();

            //ִ执行完退出
            cout<<"process: "<<getpid()<<"quit"<<endl;
            exit(0);
        }

        //父进程
        close(pipefd[0]);

        //添加channels
        std::string name="process-"+std::to_string(i);
        channels.push_back(channel(pipefd[1],id,name));//channel包含父进程对对应子进程管道的写端的文件描述符，子进程的pid和名字
  
        //从第二个进程开始，拷贝父进程的文件件描述符表的时候都会有之前创建的管道的写端的残留，所以都要关闭，所以创建完第一个子进程开始把写端的文件描述符加入数组中，方便后面所有的子进程可以删除之前所有写端的残留
        oldfds.push_back(pipefd[1]);
        sleep(1);
    }
    //初始化结束后创建了对应数量的子进程并且所有的管道都是干净的一个读端一个写端
}

//任务清单
void menu()
{
    cout<<"*********************************"<<endl;
    cout<<"********1,刷新日志־****************"<<endl;
    cout<<"********2,刷新红区****************"<<endl;
    cout<<"********3,检查更新****************"<<endl;
    cout<<"********4,刷新蓝区****************"<<endl;
    cout<<"*********************************"<<endl;
}

//父进程自动控制所有的子进程，给他们派发任务
//要考虑到负载均衡的问题：要让子进程都能均匀的得到使用才不会浪费资源
//实现浮在均衡的办法：1，轮询：每个进程轮流处理任务 2.随机：随机使用某个进程执行
void ctrlSlaver(const std::vector<channel>&channels)
{
    int cd=20;//
    while(cd)
    {
        int cmdcode=rand()%tasks.size();//让程序运行的次数

        //要选择一个进程：我们这里使用随机的方法：
        int processpos=rand()%channels.size();

        cout<<"father say:"<<"cmdcode:"<<cmdcode<<"already send to "<<channels[processpos].slaverid_<<"process name is: "<<channels[processpos].processname_<<endl;

        //发送任务给随机的那个进程：就是把任务码发送给对应进程的管道，通过我们的结构已经保存了每个子进程的管道的写入端文件描述符了
        write(channels[processpos].cmdfd_,&cmdcode,sizeof(cmdcode));


        sleep(1);//每0.1秒就产生一个新的任务派发给子进程
        cd--;
    }
}

//我们自己控制子进程执行的任务：并且这次我们使用轮询的方式
void ctrlSlaver2(const std::vector<channel>&channels)
{
   int which=0;
   while(true)
   {
    menu();
    int select=0;
    cout<<"Please Enter: ";
    //我们手动输入要执行的任务：
    std::cin>>select;
    if(select<=0||select>=5)break;
    int cmdcode=select-1;//数组中的下标要减一

    //分配给进程：
    cout<<"father say:"<<"cmdcode:"<<cmdcode<<"already send to "<<channels[which].slaverid_<<"process name is: "<<channels[which].processname_<<endl;

    write(channels[which].cmdfd_,&cmdcode,sizeof(cmdcode));
    which++;//轮流完成任务
    which%=channels.size();//which取模让which不要越界
    sleep(1);
    
   }
}

//进程池的退出
void QuitProcessPool(const std::vector<channel>&channels)
{
    for(const auto &e:channels)
    {
        //依次从子进程管道结构体中取出对应的子进程管道的结构体
        //父进程依次关闭对他们的写端即可，写关闭后，所有子进程都会从管道中读取到0从而退出执行任务，最后子进程会退出
        close(e.cmdfd_);

        //管道资源释放问题：管道资源会在所有相关进程都关闭了它们的管道读写端之后被操作系统自动释放。这是一个由操作系统内核管理的自动过程，并不需要用户显式的去释放
        //我们关闭写端后，子进程会依次退出，退出的进程操作系统会自动关闭它们所有的文件描述符资源，此时管道的读写端全部关闭，管道资源会被操作系统自动释放
        //然后退出的子进程就变成了僵尸状态：僵尸状态的时候只是进程描述符还没有被等待回收，文件描述符等资源在退出的时候就已经释放了
        //再把当前的子进程等待：
        waitpid(e.slaverid_,nullptr,0);
    }
}

void Debug(const std::vector<channel>&channels)
{
    //用来测试channels是否正确的收容了所有的子进程的结构
    for(const auto& e:channels)
    {
        cout<<e.cmdfd_<<" "<<e.slaverid_<<" "<<e.processname_<<endl;
    }
}

int  main()
{
    LoadTask(tasks);//加载任务到对应的容器中

    srand(time(nullptr)^getpid()^1023);
    //这种种子设置方法比简单地使用 time(nullptr)作为种子要更好，因为它还考虑了进程ID，这意味着即使在非常短的时间内多次启动程序，由于异或了id和一个常数，使得随机的结果更加分散
    //就是为了让数据更加随机，这样就可以把任务随机分配给不同的进程，这就是传说中的“负载均衡”，让每个进程都可以更加平均的执行任务
    std::vector<channel>channels;//组织我们的子进程的结构放在容器中
    InitProcessPool(channels);//初始化一下容器

    //开始控制子进程：
    ctrlSlaver2(channels);

    //清理收尾：
    QuitProcessPool(channels);

    return 0;
}

//小结：所有的一切都是靠一个装着process属性结构的channels数组实现的，把所有的子进程用数组组织在一起，让我们可以调用所有的进程，并且对它们进行控制，这就是进程池的本质实现
//进程池的实现离不开先描述后组织的思维逻辑，本质上进程池就是使用数组维护的一张进程表的结构
